package com.ipan.kits.web;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;

import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import com.ipan.kits.mapper.JsonMapper;
import com.ipan.kits.reflect.ReflectionUtil;
import com.ipan.kits.text.EncodeUtil;
import com.ipan.kits.xconverter.XTypeConverter;

/**
 * Http与Servlet工具类.
 * 
 * @author iPan
 * @version 2015-05-29
 */
public class ServletUtil {
	
	/**
     * 定义移动端请求的所有可能类型
     */
    private final static String[] agent = { "Android", "iPhone", "iPod", "iPad", "Windows Phone", "MQQBrowser" };
	
	//-- header 常量定义 --//
	private static final String HEADER_ENCODING = "encoding";
	private static final String HEADER_NOCACHE = "no-cache";
	private static final String DEFAULT_ENCODING = "UTF-8";
	private static final boolean DEFAULT_NOCACHE = true;
	private static JsonMapper _JSON = JsonMapper.getDefaultJsonMapper();

	//-- Content Type 定义 --//
	public static final String TEXT_TYPE = "text/plain";
	public static final String JSON_TYPE = "application/json";
	public static final String XML_TYPE = "text/xml";
	public static final String HTML_TYPE = "text/html";
	public static final String JS_TYPE = "text/javascript";
	public static final String EXCEL_TYPE = "application/vnd.ms-excel";
	public static final String EXCELX_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
	public static final String DEFAULT_TYPE = "application/octet-stream";

	//-- Header 定义 --//
	public static final String AUTHENTICATION_HEADER = "Authorization";

	//-- 常用数值定义 --//
	public static final long ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
	
	//-- 代理上网头部 --//
	private static final String[] PROXY_CLIENT_HEADERS = {
            "X-Forwarded-For", // Squid 服务代理
            "Proxy-Client-IP", // apache 服务代理
            "WL-Proxy-Client-IP", // weblogic 服务代理
            "HTTP_X_FORWARDED_FOR",
            "HTTP_X_FORWARDED",
            "HTTP_X_CLUSTER_CLIENT_IP",
            "HTTP_CLIENT_IP", // 有些代理服务器
            "HTTP_FORWARDED_FOR",
            "HTTP_FORWARDED",
            "HTTP_VIA",
            "REMOTE_ADDR",
            "X-Real-IP" // nginx服务代理
    };
	
	/**
     * 获取用户的真正IP地址
     *
     * @param request request对象
     * @return 返回用户的IP地址
     */
    public static String getClientIp(HttpServletRequest request) {
        for (String header : PROXY_CLIENT_HEADERS) {
            String ip = request.getHeader(header);
            if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
            	int index = ip.indexOf(',');
            	if (index > 0) {
            		ip = ip.substring(0, index);
            	}
            	return ip;
            }
        }
        return request.getRemoteAddr();
    }

	/**
	 * 设置客户端缓存过期时间 的Header.
	 */
	public static void setExpiresHeader(HttpServletResponse response, long expiresSeconds) {
		//Http 1.0 header
		response.setDateHeader("Expires", System.currentTimeMillis() + expiresSeconds * 1000);
		//Http 1.1 header
		response.setHeader("Cache-Control", "private, max-age=" + expiresSeconds);
	}

	/**
	 * 设置禁止客户端缓存的Header.
	 */
	public static void setDisableCacheHeader(HttpServletResponse response) {
		//Http 1.0 header
		response.setDateHeader("Expires", 1L);
		response.addHeader("Pragma", "no-cache");
		//Http 1.1 header
		response.setHeader("Cache-Control", "no-cache, no-store, max-age=0");
	}

	/**
	 * 设置LastModified Header.
	 */
	public static void setLastModifiedHeader(HttpServletResponse response, long lastModifiedDate) {
		response.setDateHeader("Last-Modified", lastModifiedDate);
	}

	/**
	 * 设置Etag Header.
	 */
	public static void setEtag(HttpServletResponse response, String etag) {
		response.setHeader("ETag", etag);
	}

	/**
	 * 根据浏览器If-Modified-Since Header, 计算文件是否已被修改.
	 * 
	 * 如果无修改, checkIfModify返回false ,设置304 not modify status.
	 * 
	 * @param lastModified 内容的最后修改时间.
	 */
	public static boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response,
			long lastModified) {
		long ifModifiedSince = request.getDateHeader("If-Modified-Since");
		if ((ifModifiedSince != -1) && (lastModified < ifModifiedSince + 1000)) {
			response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
			return false;
		}
		return true;
	}

	/**
	 * 根据浏览器 If-None-Match Header, 计算Etag是否已无效.
	 * 
	 * 如果Etag有效, checkIfNoneMatch返回false, 设置304 not modify status.
	 * 
	 * @param etag 内容的ETag.
	 */
	public static boolean checkIfNoneMatchEtag(HttpServletRequest request, HttpServletResponse response, String etag) {
		String headerValue = request.getHeader("If-None-Match");
		if (headerValue != null) {
			boolean conditionSatisfied = false;
			if (!"*".equals(headerValue)) {
				StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");

				while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
					String currentToken = commaTokenizer.nextToken();
					if (currentToken.trim().equals(etag)) {
						conditionSatisfied = true;
					}
				}
			} else {
				conditionSatisfied = true;
			}

			if (conditionSatisfied) {
				response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
				response.setHeader("ETag", etag);
				return false;
			}
		}
		return true;
	}

	/**
	 * 设置让浏览器弹出下载对话框的Header.
	 * 
	 * @param fileName 下载后的文件名.
	 */
	public static void setFileDownloadHeader(HttpServletResponse response, String fileName) {
		try {
			//中文文件名支持
			String encodedfileName = new String(fileName.getBytes("GBK"), "ISO8859-1");
			response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedfileName + "\"");
		} catch (UnsupportedEncodingException e) {
		}
	}
	
	/**
	 * 下载文件Http Head设置
	 * 新增：根据不同浏览器自动判断编码规则
	 */
	public static void setFileDownloadHeader(HttpServletRequest request, HttpServletResponse response, String fileName, String contentType) {
		response.setHeader("Accept-Ranges", "bytes");
		response.setHeader("Content-disposition", "attachment; " + encodeFileName(request, fileName));
		String cnt = (StringUtils.isBlank(contentType)) ? MimeUtil.getContentType(fileName) : contentType;
		response.setContentType(StringUtils.isBlank(cnt) ? DEFAULT_TYPE : cnt);
	}
	public static void setFileDownloadHeader(HttpServletRequest request, HttpServletResponse response, String fileName) {
		setFileDownloadHeader(request, response, fileName, DEFAULT_TYPE);
	}
	/**
	 * 依据浏览器判断编码规则
	 */
	public static String encodeFileName(HttpServletRequest request, String fileName) {
		String userAgent = request.getHeader("User-Agent");
		try {
			String encodedFileName = URLEncoder.encode(fileName, "UTF8");
			// 如果没有UA，则默认使用IE的方式进行编码
			if (userAgent == null) {
				return "filename=\"" + encodedFileName + "\"";
			}
			
			userAgent = userAgent.toLowerCase();
			// IE浏览器，只能采用URLEncoder编码
			if (userAgent.indexOf("msie") != -1) {
				return "filename=\"" + encodedFileName + "\"";
			}
			
			// Opera浏览器只能采用filename*
			if (userAgent.indexOf("opera") != -1) {
				return "filename*=UTF-8''" + encodedFileName;
			}
			
			// Safari浏览器，只能采用ISO编码的中文输出,Chrome浏览器，只能采用MimeUtility编码或ISO编码的中文输出
			if (userAgent.indexOf("safari") != -1 || userAgent.indexOf("applewebkit") != -1 || userAgent.indexOf("chrome") != -1) {
				return "filename=\"" + new String(fileName.getBytes("UTF-8"), "ISO8859-1") + "\"";
			}
			
			// FireFox浏览器，可以使用MimeUtility或filename*或ISO编码的中文输出
			if (userAgent.indexOf("mozilla") != -1) {
				return "filename*=UTF-8''" + encodedFileName;
			}
			
			return "filename=\"" + encodedFileName + "\"";
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 取得带相同前缀的Request Parameters.
	 * 
	 * 返回的结果的Parameter名已去除前缀.
	 */
	@SuppressWarnings({"rawtypes" })
	public static Map<String, Object> getParametersStartingWith(ServletRequest request, String prefix) {
		Validate.notNull(request, "Request must not be null");
		Enumeration paramNames = request.getParameterNames();
		Map<String, Object> params = new TreeMap<String, Object>();
		if (prefix == null) {
			prefix = "";
		}
		while (paramNames != null && paramNames.hasMoreElements()) {
			String paramName = (String) paramNames.nextElement();
			if ("".equals(prefix) || paramName.startsWith(prefix)) {
				String unprefixed = paramName.substring(prefix.length());
				String[] values = request.getParameterValues(paramName);
				if (values == null || values.length == 0) {
					// Do nothing, no values found at all.
				} else if (values.length > 1) {
					params.put(unprefixed, values);
				} else {
					params.put(unprefixed, values[0]);
				}
			}
		}
		return params;
	}

	/**
	 * 对Http Basic验证的 Header进行编码.
	 */
	public static String encodeHttpBasic(String userName, String password) {
		String encode = userName + ":" + password;
		return "Basic " + EncodeUtil.encodeBase64(encode.getBytes());
	}
	
	/**
	 * 是否是AJAX请求
	 */
	public static boolean isAjax(HttpServletRequest request) {
		String accept = request.getHeader("accept");
        if (accept != null && accept.indexOf("application/json") != -1) {
            return true;
        }

        String xRequestedWith = request.getHeader("X-Requested-With");
        if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) {
            return true;
        }
        return false;
	}
	
	/**
	 * 分析并设置contentType与headers.
	 */
	private static void initResponseHeader(final HttpServletResponse response, final String contentType, final String... headers) {
		//分析headers参数
		String encoding = DEFAULT_ENCODING;
		boolean noCache = DEFAULT_NOCACHE;
		for (String header : headers) {
			String headerName = StringUtils.substringBefore(header, ":");
			String headerValue = StringUtils.substringAfter(header, ":");

			if (StringUtils.equalsIgnoreCase(headerName, HEADER_ENCODING)) {
				encoding = headerValue;
			} else if (StringUtils.equalsIgnoreCase(headerName, HEADER_NOCACHE)) {
				noCache = Boolean.parseBoolean(headerValue);
			} else if (StringUtils.equalsIgnoreCase(headerName, "Access-Control-Allow-Origin")) {
				response.setHeader("Access-Control-Allow-Origin", "*");
			} else {
				throw new IllegalArgumentException(headerName + "不是一个合法的header类型");
			}
		}

		//设置headers参数
		String fullContentType = contentType + ";charset=" + encoding;
		response.setContentType(fullContentType);
		if (noCache) {
			ServletUtil.setDisableCacheHeader(response);
		}
	}
	
	/**
	 * 直接输出内容的简便函数.
	 * eg.
	 * render("text/plain", "hello", "encoding:GBK");
	 * render("text/plain", "hello", "no-cache:false");
	 * render("text/plain", "hello", "encoding:GBK", "no-cache:false");
	 * 
	 * @param headers 可变的header数组，目前接受的值为"encoding:"或"no-cache:",默认值分别为UTF-8和true.
	 */
	public static void render(final HttpServletResponse response, final String contentType, final String content, final String... headers) {
		initResponseHeader(response, contentType, headers);
		try {
			response.getWriter().write(content);
			response.getWriter().flush();
		} catch (IOException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * 直接输出文本.
	 * @see #render(String, String, String...)
	 */
	public static void renderText(final HttpServletResponse response, final String text, final String... headers) {
		render(response, ServletUtil.TEXT_TYPE, text, headers);
	}

	/**
	 * 直接输出HTML.
	 * @see #render(String, String, String...)
	 */
	public static void renderHtml(final HttpServletResponse response, final String html, final String... headers) {
		render(response, ServletUtil.HTML_TYPE, html, headers);
	}

	/**
	 * 直接输出XML.
	 * @see #render(String, String, String...)
	 */
	public static void renderXml(final HttpServletResponse response, final String xml, final String... headers) {
		render(response, ServletUtil.XML_TYPE, xml, headers);
	}

	/**
	 * 直接输出JSON.
	 * 
	 * @param jsonString json字符串.
	 * @see #render(String, String, String...)
	 */
	public static void renderJson(final HttpServletResponse response, final String jsonString, final String... headers) {
		render(response, ServletUtil.JSON_TYPE, jsonString, headers);
	}

	/**
	 * 直接输出JSON,使用Jackson转换Java对象.
	 * 
	 * @param data 可以是List<POJO>, POJO[], POJO, 也可以Map名值对.
	 * @see #render(String, String, String...)
	 */
	public static void renderJson(final HttpServletResponse response, final Object data, final String... headers) {
		initResponseHeader(response, ServletUtil.JSON_TYPE, headers);
		try {
			String jsonStr = _JSON.toJson(data);
			response.getWriter().print(jsonStr);
		} catch (IOException e) {
			throw new IllegalArgumentException(e);
		}
	}

	/**
	 * 直接输出支持跨域Mashup的JSONP.
	 * 
	 * @param callbackName callback函数名.
	 * @param object Java对象,可以是List<POJO>, POJO[], POJO ,也可以Map名值对, 将被转化为json字符串.
	 */
	public static void renderJsonp(final HttpServletResponse response, final String callbackName, final Object object, final String... headers) {
		String jsonString = null;
		jsonString = _JSON.toJson(object);
		String result = new StringBuilder().append(callbackName).append("(").append(jsonString).append(");").toString();

		//渲染Content-Type为javascript的返回内容,输出结果为javascript语句, 如callback197("{html:'Hello World!!!'}");
		render(response, ServletUtil.JS_TYPE, result, headers);
	}

	/**
	 * 将HttpServletRequest注入到JavaBean
	 */
	public static <T> T injectBean(Class<T> beanClass, HttpServletRequest request, boolean skipConvertError) {
		return (T)injectBean(beanClass, null, request, skipConvertError);
	}
	@SuppressWarnings("unchecked")
	public static <T> T injectBean(Class<T> beanClass, String beanName, HttpServletRequest request, boolean skipConvertError) {
		Object bean = ReflectionUtil.createInstance(beanClass);
		String modelNameAndDot = StringUtils.isNotBlank(beanName) ? beanName + "." : null;
		XTypeConverter converter = XTypeConverter.me(); // 默认类型转换器
		Map<String, String[]> parasMap = request.getParameterMap();
		Method[] methods = beanClass.getMethods();
		for (Method method : methods) {
			String methodName = method.getName();
			if (methodName.startsWith("set") == false || methodName.length() <= 3) {	// only setter method
				continue;
			}
			Class<?>[] types = method.getParameterTypes();
			if (types.length != 1) {						// only one parameter
				continue;
			}
			
			String attrName = StringUtils.capitalize(methodName.substring(3));
			String paraName = modelNameAndDot != null ? modelNameAndDot + attrName : attrName;
			if (parasMap.containsKey(paraName)) {
				try {
					String paraValue = request.getParameter(paraName);
					Object value = paraValue != null ? converter.convert(types[0], paraValue) : null;
					method.invoke(bean, value);
				} catch (Exception e) {
					if (skipConvertError == false) {
						throw new RuntimeException(e);
					}
				}
			}
		}
		
		return (T)bean;
	}
	
	// -- cookie 操作-- //
	public static Cookie getCookieObject(HttpServletRequest request, String name) {
		Cookie[] cookies = request.getCookies();
		if (cookies != null)
			for (Cookie cookie : cookies)
				if (cookie.getName().equals(name))
					return cookie;
		return null;
	}
	
	public static String getCookie(HttpServletRequest request, String name, String defaultValue) {
		Cookie cookie = getCookieObject(request, name);
		return cookie != null ? cookie.getValue() : defaultValue;
	}
	
	public static String getCookie(HttpServletRequest request, String name) {
		return getCookie(request, name, null);
	}

	public static void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, boolean isHttpOnly) {
		doSetCookie(response, name, value, maxAgeInSeconds, null, null, isHttpOnly);
	}
	public static void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {
		doSetCookie(response, name, value, maxAgeInSeconds, null, null, null);
	}
	public static void setCookie(HttpServletResponse response, Cookie cookie) {
		response.addCookie(cookie);
	}
	public static void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, boolean isHttpOnly) {
		doSetCookie(response, name, value, maxAgeInSeconds, path, null, isHttpOnly);
	}
	public static void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path) {
		doSetCookie(response, name, value, maxAgeInSeconds, path, null, null);
	}
	public static void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, String domain, boolean isHttpOnly) {
		doSetCookie(response, name, value, maxAgeInSeconds, path, domain, isHttpOnly);
	}
	
	public static void removeCookie(HttpServletResponse response, String name) {
		doSetCookie(response, name, null, 0, null, null, null);
	}
	public static void removeCookie(HttpServletResponse response, String name, String path) {
		doSetCookie(response, name, null, 0, path, null, null);
	}
	public static void removeCookie(HttpServletResponse response, String name, String path, String domain) {
		doSetCookie(response, name, null, 0, path, domain, null);
	}
	
	private static void doSetCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds, String path, String domain, Boolean isHttpOnly) {
		Cookie cookie = new Cookie(name, value);
		cookie.setMaxAge(maxAgeInSeconds);
		// set the default path value to "/"
		if (path == null) {
			path = "/";
		}
		cookie.setPath(path);
		
		if (domain != null) {
			cookie.setDomain(domain);
		}
		if (isHttpOnly != null) {
			cookie.setHttpOnly(isHttpOnly);
		}
		response.addCookie(cookie);
	}
	
	/**
	 * 判断User-Agent 是不是来自于手机
	 */
	public static boolean checkAgentIsMobile(String ua) {
		boolean flag = false;
		if (!ua.contains("Windows NT") || (ua.contains("Windows NT") && ua.contains("compatible; MSIE 9.0;"))) {
			// 排除 苹果桌面系统
			if (!ua.contains("Windows NT") && !ua.contains("Macintosh")) {
				for (String item : agent) {
					if (ua.contains(item)) {
						flag = true;
						break;
					}
				}
			}
		}
		return flag;
	}
	
	/**
     * 获取完整的请求路径，包括：域名，端口，上下文访问路径
     */
	public static String getUrl(HttpServletRequest request) {
        StringBuffer url = request.getRequestURL();
        String contextPath = request.getServletContext().getContextPath();
        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
    }
	
}
